# This file is part of Appartition, a lightweight and portable backup tool.
# Copyright (C) 2012-2014 Simon Grieger
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

###########################################################################
# Package: Appartition::Core
#
# This package provides core functionality.
###########################################################################

package Appartition::Core;

use 5.006;
use strict;
use warnings;

use Appartition;
use Appartition::FileSystem;
use Appartition::Shell;
use Appartition::Utilities;

###########################################################################
# Group: High-level Functions
###########################################################################

# Function: initialize
#
# Prepares the workspace for backup and restore. Creates the workspace
# directory if necessary, retrieves file system information and mounts
# selected file systems.
#
# Returns:
#
#   True.
sub initialize #()
  {
    Appartition::Shell::show_debug_output('Workspace is "' . $Appartition::WORKSPACE . '"');
    Appartition::Shell::show_debug_output('Backup destination is "' . $Appartition::BACKUP . '"');

    Appartition::Utilities::create_directory($Appartition::WORKSPACE)
      unless (defined $Appartition::OPTION_VALUES->{'workspace'});
    premount_root_file_system($Appartition::OPTION_VALUES->{'root'})
      if (defined $Appartition::OPTION_VALUES->{'root'});
    get_file_system_information();
    perform_sanity_checks();
    mount_selected_file_systems()
      unless ($Appartition::OPTION_VALUES->{'empty-workspace'});

    Appartition::Shell::show_notification('Workspace prepared');

    return 1;
  }

# Function: deinitialize
#
# Cleans up the workspace. Unmounts all file systems, and removes the
# temporary root mount point, if applicable. Catches all exceptions raised
# during cleanup and shows them as warnings.
#
# Returns:
#
#   True.
sub deinitialize #()
  {
    local $@;

    Appartition::Shell::show_progress('Cleaning up');

    eval {
      unmount_all_file_systems();
      Appartition::Utilities::remove_directory($Appartition::WORKSPACE)
        unless (defined $Appartition::OPTION_VALUES->{'workspace'});
    };

    warn $@ if ($@);

    return 1;
  }

###########################################################################
# Group: Functions Managing File System Information
###########################################################################

# Function: get_file_system_information
#
# Retrieves file system information. Reads all file systems from the fstab
# file, selects file systems to be included in backups, and finally adds
# them to the list of known file systems.
#
# Returns:
#
#   True.
sub get_file_system_information #()
  {
    Appartition::Shell::show_debug_output('Attempting to read fstab file "' . $Appartition::FSTAB . '"');

    if (-f $Appartition::FSTAB) {
      Appartition::Shell::show_progress('Retrieving file system information');

      my @fs = read_fstab($Appartition::FSTAB);
      select_file_systems(@fs);
      add_file_systems(@fs);
    }
    else {
      warn 'The fstab file has been specified explicitly, but could not be found.' . "\n"
        if (defined $Appartition::OPTION_VALUES->{'fstab'});
      Appartition::Shell::show_notification('File system information not available');
    }

    return 1;
  }

# Function: perform_sanity_checks
#
# Performs sanity checks on the file system information.
#
# Returns:
#
#   True.
sub perform_sanity_checks #()
  {
    # Check whether any file systems were found.
    if (not get_file_systems() and
        not $Appartition::OPTION_VALUES->{'empty-workspace'}) {
      warn 'Could not find any file systems' . "\n",
           'Rerun with "--root" to specify a root file system, or "--fstab" to provide a valid fstab file' . "\n";
    }

    return 1;
  }

# Function: read_fstab
#
# Reads file system information from an fstab file.
#
# Parameters:
#
#   file - Translated path (see <Appartition::Utilities::translate_path()>
#          for details) to the fstab file.
#
# Returns:
#
#   A (possibly empty) list of file systems (objects of class
#   <Appartition::FileSystem>).
sub read_fstab #(file)
  {
    my ($file) = @_;
    my @fs = ();

    open(my $fstab, '<', $file)
      or die 'Unable to open "' . $file . '": ' . $! . "\n";

    while (my $entry = <$fstab>) {
      # Strip comments and leading/trailing whitespace.
      $entry =~ s'#.*$''o;
      $entry =~ s'^\s+''o;
      $entry =~ s'\s+$''o;

      next if ($entry eq '');

      Appartition::Shell::show_debug_output('Read fstab entry "' . $entry . '"');
      push(@fs, Appartition::FileSystem->new_from_fstab_entry($entry));
    }

    close($fstab);
    return (@fs);
  }

# Function: select_file_systems
#
# Selects those file systems that are supposed to be included in backups.
#
# Parameters:
#
#   list - List of file systems (objects of class
#          <Appartition::FileSystem>). The list is allowed to be empty.
#
# Returns:
#
#   A (possibly empty) list of the selected file systems.
sub select_file_systems #(list)
  {
    my @list = ();

    foreach my $fs (@_) {
      next if ($fs->mount_point() eq '/' and
               defined $Appartition::OPTION_VALUES->{'root'});
      next if (not $Appartition::OPTION_VALUES->{'include-noauto'} and
               $fs->has_option('noauto') and
               (not $fs->mount_point() eq '/boot' or
                $Appartition::OPTION_VALUES->{'ignore-boot'}));
      next if (grep({$_ eq $fs->type()} @Appartition::IGNORED_FS_TYPES));
      next if ($fs->mount_point() eq 'none');

      # The file system fell through all the filters above, so select it.
      $fs->select();
      push(@list, $fs);

      Appartition::Shell::show_debug_output('Selected fs "' . $fs->spec() . '"');
    }

    return (@list);
  }

###########################################################################
# Group: Functions for Mounting and Unmounting File Systems
###########################################################################

# Function: premount_root_file_system
#
# Pre-mounts a root file system read-only. The file system will be added to
# the list of known file systems.
#
# Parameters:
#
#   fs - File system specification. See <Appartition::FileSystem->new()>
#        for details.
#
# Returns:
#
#   An object of class <Appartition::FileSystem>.
sub premount_root_file_system #(fs)
  {
    my ($fs) = @_;
    my $root;

    $root = Appartition::FileSystem->new($fs, '/');
    add_file_systems($root);
    $root->mount_read_only();

    return $root;
  }

# Function: mount_selected_file_systems
#
# Mounts all selected file systems read-only.
#
# Returns:
#
#   True.
sub mount_selected_file_systems #()
  {
    foreach my $fs (filter_selected_file_systems(get_file_systems())) {
      $fs->mount_read_only();
    }

    return 1;
  }

# Function: remount_all_file_systems_read_write
#
# Remounts all currently mounted file systems read-write.
#
# Returns:
#
#   True.
sub remount_all_file_systems_read_write #()
  {
    foreach my $fs (get_mounted_file_systems()) {
      $fs->remount_read_write();
    }

    return 1;
  }

# Function: unmount_all_file_systems
#
# Unmounts all currently mounted file systems.
#
# Returns:
#
#   True.
sub unmount_all_file_systems #()
  {
    foreach my $fs (reverse get_mounted_file_systems()) {
      $fs->unmount();
    }

    return 1;
  }

###########################################################################
# Group: Functions for Accessing and Modifying the List of File Systems
###########################################################################

# Function: get_file_systems
#
# Gets a list of all known file systems.
#
# Returns:
#
#   A (possibly empty) list of file systems (objects of class
#   <Appartition::FileSystem>).
sub get_file_systems #()
  {
    my @list = ();

    foreach my $fs (@$Appartition::FILE_SYSTEMS) {
      push(@list, $fs) if (defined $fs);
    }

    return (@list);
  }

# Function: get_ids_of_file_systems
#
# Gets the IDs of all known file systems. See <Appartition::FILE_SYSTEMS>
# for a description of file system IDs.
#
# Returns:
#
#   A (possibly empty) list of IDs.
sub get_ids_of_file_systems #()
  {
    my @list = ();

    for (my $i = 0; $i < @$Appartition::FILE_SYSTEMS; $i++) {
      push(@list, $i) if (defined $Appartition::FILE_SYSTEMS->[$i]);
    }

    return (@list);
  }

# Function: get_mounted_file_systems
#
# Gets a list of all mounted file systems, in the order they were mounted.
#
# Returns:
#
#   A (possibly empty) list of file systems (objects of class
#   <Appartition::FileSystem>).
sub get_mounted_file_systems #()
  {
    return (sort {$a->stamp() <=> $b->stamp()}
                 filter_mounted_file_systems(get_file_systems()));
  }

# Function: add_file_systems
#
# Adds one or more entries to the list of known file systems. The file
# systems will be assigned unique IDs. See <Appartition::FILE_SYSTEMS> for
# a description of file system IDs.
#
# Parameters:
#
#   list - List of file systems (objects of class
#          <Appartition::FileSystem>). The list is allowed to be empty.
#
# Returns:
#
#   A (possibly empty) list of the added file systems' IDs.
sub add_file_systems #(list)
  {
    my @ids = ();

    foreach my $fs (@_) {
      my $id = get_id_of_file_system_if_available($fs);

      unless (defined $id) {
        # Add the file system to the list and assign the ID to the file
        # system object. Although this approach is prone to inconsistencies
        # between the IDs determined by the list of known file systems and
        # the IDs stored in the file system objects, it avoids the
        # time-consuming retranslation from a file system to its ID, which
        # would otherwise be necessary in the two functions
        # get_id_of_file_system() and get_id_of_file_system_if_available().

        push(@$Appartition::FILE_SYSTEMS, $fs);
        $id = $#$Appartition::FILE_SYSTEMS;
        $fs->id($id);
      }
      else {
        warn 'File system "' . $fs->spec() . '" ' .
             'has already been added to the list of known file systems ' .
             '(ID ' . $id . ')' . "\n";
      }

      push(@ids, $id);
    }

    return (@ids);
  }

# Function: get_id_of_file_system
#
# Retrieves a file system's ID. See <Appartition::FILE_SYSTEMS> for a
# description of file system IDs.
#
# Parameters:
#
#   fs - File system (an object of class <Appartition::FileSystem>).
#
# Returns:
#
#   The file system's ID.
sub get_id_of_file_system #(fs)
  {
    my ($fs) = @_;
    my $id = get_id_of_file_system_if_available($fs);

    die 'File system "' . $fs->spec() . '" ' .
        'has not been added to the list of known file systems' . "\n"
      unless (defined $id);

    return $id;
  }

# Function: get_id_of_file_system_if_available
#
# Retrieves a file system's ID. Does not raise an exception if the given
# file system has not been added to the list of known file systems, as
# opposed to <get_id_of_file_system()>. See <Appartition::FILE_SYSTEMS> for
# a description of file system IDs.
#
# Parameters:
#
#   fs - File system (an object of class <Appartition::FileSystem>).
#
# Returns:
#
#   The file system's ID, or undef if the file system has not been added to
#   the list of known file systems.
sub get_id_of_file_system_if_available #(fs)
  {
    my ($fs) = @_;
    return $fs->id();
  }

# Function: get_file_system_by_id
#
# Finds a file system by its ID. See <Appartition::FILE_SYSTEMS> for a
# description of file system IDs.
#
# Parameters:
#
#   id - File system ID.
#
# Returns:
#
#   The corresponding file system (an object of class
#   <Appartition::FileSystem>).
sub get_file_system_by_id #(id)
  {
    my ($id) = @_;
    my $fs = get_file_system_by_id_if_available($id);

    die 'Invalid file system ID: ' . $id . "\n"
      unless (defined $fs);

    return $fs;
  }

# Function: get_file_system_by_id_if_available
#
# Finds a file system by its ID. Does not raise an exception if the given
# ID is invalid, as opposed to <get_file_system_by_id()>. See
# <Appartition::FILE_SYSTEMS> for a description of file system IDs.
#
# Parameters:
#
#   id - File system ID.
#
# Returns:
#
#   The corresponding file system (an object of class
#   <Appartition::FileSystem>), or undef if the ID is invalid.
sub get_file_system_by_id_if_available #(id)
  {
    my ($id) = @_;

    die '"' . $id . '" is not a file system ID' . "\n"
      unless ($id =~ m'^[0-9]+$'o);

    return $Appartition::FILE_SYSTEMS->[$id];
  }

# Function: get_file_system_by_mount_point
#
# Finds a file system by mount option.
#
# Parameters:
#
#   mp - Mount point.
#
# Returns:
#
#   A list of file systems with the given mount point (objects of class
#   <Appartition::FileSystem>).
sub get_file_system_by_mount_point #(mp)
  {
    my $mp = Appartition::Utilities::strip_trailing_slashes(shift);
    my @list = get_file_system_by_mount_point_if_available($mp);

    die 'No file systems with mount point "' . $mp . '"' . "\n"
      unless (@list);

    return (@list);
  }

# Function: get_file_system_by_mount_point_if_available
#
# Finds a file system by mount option. Does not raise an exception if there
# are no known file systems with the given mount point, as opposed to
# <get_file_system_by_mount_point()>.
#
# Parameters:
#
#   mp - Mount point.
#
# Returns:
#
#   A list of file systems with the given mount point (objects of class
#   <Appartition::FileSystem>), or the empty list if there are no such file
#   systems.
sub get_file_system_by_mount_point_if_available #(mp)
  {
    my $mp = Appartition::Utilities::strip_trailing_slashes(shift);
    my @list = ();

    foreach my $fs (get_file_systems()) {
      push(@list, $fs) if ($fs->mount_point() eq $mp);
    }

    return (@list);
  }

# Function: get_root_file_system
#
# Finds the root file system.
#
# Returns:
#
#   A list of root file systems (objects of class
#   <Appartition::FileSystem>).
sub get_root_file_system #()
  {
    return get_file_system_by_mount_point('/');
  }

# Function: get_root_file_system_if_available
#
# Finds the root file system. Does not raise an exception if there are no
# known root file systems, as opposed to <get_root_file_system()>.
#
# Returns:
#
#   A list of root file systems (objects of class
#   <Appartition::FileSystem>), or the empty list if there are no such file
#   systems.
sub get_root_file_system_if_available #()
  {
    return get_file_system_by_mount_point_if_available('/');
  }

# Function: filter_selected_file_systems
#
# Filters selected file systems from the given list of file systems.
#
# Parameters:
#
#   list - List of file systems (objects of class
#          <Appartition::FileSystem>). The list is allowed to be empty.
#
# Returns:
#
#   A (possibly empty) list of only the selected file systems.
sub filter_selected_file_systems #(list)
  {
    my @list = ();

    foreach my $fs (@_) {
      push(@list, $fs) if ($fs->is_selected());
    }

    return (@list);
  }

# Function: filter_mounted_file_systems
#
# Filters mounted file systems from the given list of file systems.
#
# Parameters:
#
#   list - List of file systems (objects of class
#          <Appartition::FileSystem>). The list is allowed to be empty.
#
# Returns:
#
#   A (possibly empty) list of only the mounted file systems.
sub filter_mounted_file_systems #(list)
  {
    my @list = ();

    foreach my $fs (@_) {
      push(@list, $fs) if ($fs->is_mounted());
    }

    return (@list);
  }

###########################################################################
# Group: Functions for Executing System Commands
###########################################################################

# Function: exec_mount
#
# Executes the system's mount command to mount a file system. *Warning:* Do
# not use this function directly. Call one of the file system's methods
# instead.
#
# Parameters:
#
#   fs      - File system (an object of class <Appartition::FileSystem>).
#   remount - True to indicate that the file system should be remounted,
#             false if it is the initial mount.
#
# Returns:
#
#   True.
sub exec_mount #(fs, remount)
  {
    my ($fs, $remount) = @_;

    # TODO

    return 1;
  }

# Function: exec_umount
#
# Executes the system's umount command to unmount a file system. *Warning:*
# Do not use this function directly. Call one of the file system's methods
# instead.
#
# Parameters:
#
#   fs - File system (an object of class <Appartition::FileSystem>).
#
# Returns:
#
#   True.
sub exec_umount #(fs)
  {
    my ($fs) = @_;

    # TODO

    return 1;
  }

1;
